#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "../inc/hash.h"
#include "../inc/defs.h"
#include "../inc/log.h"

#define	TABLE_INIT_SIZE	13 /* Numero primo */
#define	LIMIT		0.6

typedef enum {PHYSICAL, OCCUPIED, LOGICAL} statusT;

typedef struct
{
	hashElementT	data;
	statusT			status;
} htableT;

struct hashCDT
{
	htableT *		htable;
	int				datasize;
	int				tabledim;
	int				qtty;
	htableT *		actual;
	int 			index;
	hashCmp			hcmp;
	hashKey			hkey;
};

static int InitTable(hashADT hash);
static int Rehash(hashADT hash);	

hashADT	
NewHash(int esize, hashCmp hcmp, hashKey hkey)
{
	hashADT aux;
	
	if ( esize <= 0 || Precondicion(hcmp) || Precondicion(hkey) )
		return NULL;
		
	if ( (aux = calloc(1, sizeof(struct hashCDT))) == NULL )
	{
		fprintf(stderr, "Not enough memory to create hash table\n");
		return NULL;
	}
	
	aux -> datasize = esize;
	aux -> hcmp = hcmp;
	aux -> hkey = hkey;
	
	return aux;
}

int 
HInsert(hashADT hash, hashElementT data)
{
	int firstLogic = -1;
	int iter = 0;
	int pos, ok = 1;
	int equal;
	double load;
	htableT	* htable;
	
	if ( Precondicion(hash) || Precondicion(data) ) 
	{
		fprintf(stderr, "Invalid arguments to function HInsert\n");
		return 0;
	}
	
	/* Si es la primera insercion sobre la tabla, se inicia la tabla */
	if ( HTableIsEmpty(hash) )
		if ( (hash -> tabledim = InitTable(hash)) == 0 )
			return 0;
	
	/* Obtiene la clave hasheada */
	pos = hash -> hkey(data, GetSlots(hash));
	
	htable = hash -> htable;
	
	/* Mientras no se recorra toda la tabla y (la posicion hasheada este ocupada por
	 * un elemento distinto al que se quiere insertar o esa ranura tenga una baja logica
	 * continuar buscando una posicion vacia.
	 */
	while ( iter != GetSlots(hash) && ( (htable[pos].status == OCCUPIED && hash -> hcmp(htable[pos].data, data) != 0) || htable[pos].status == LOGICAL ) )
	{
		/* Si en el recorrido en busca de una celda vacia se encuentra una baja logica
		 * y es la primera, se almacena dicha posicion para insertar alli mas tarde
		 */
		if (hash -> htable[pos].status == LOGICAL)
			if (firstLogic == -1)
				firstLogic = pos;
		
		pos = (pos + 1) % hash -> tabledim;
	}

	

	/* Elemento repetido, no se inserta */
	if (htable[pos].status == OCCUPIED)
	{
		equal = hash -> hcmp(htable[pos].data, data);
		
		if (equal == 0)
			return 0;
	}

	else
	{
		/* Se recorrio toda la tabla, no hay mas lugar, no se puede insertar */
		if (iter == hash -> tabledim)
			return 0;

		else
		{
			/* Se encontro lugar, se insertar en la primera baja logica si es que la hubo
			 * o en la celda vacia */
			if (firstLogic != -1)
			{
				htable[firstLogic].status = OCCUPIED;
				
				/* no reserva memoria pues en baja logica no se borra al elemento */				
				memcpy(htable[firstLogic].data, data, hash -> datasize);
			}
			else
			{
				hash -> htable[pos].status = OCCUPIED;
				if ( (htable[pos].data = malloc(hash -> datasize)) == NULL )
				{
					Fatal("HInsert@hashADT.c - not enough memory\n");
					return 0;
				}
				memcpy(htable[pos].data, data, hash -> datasize);
			}
		}
	}

	hash -> qtty++;
	
	/* Se hace el chequeo para saber si es necesario agrandar la tabla segun el factor de
	 * carga
	 */
	load = (double)hash -> qtty / GetSlots(hash);

	if (load > LIMIT)
		ok = Rehash(hash);

	return ok;
}

int
Lookup(hashADT hash, hashElementT data)
{
	int iter = 0;
	int pos;
	htableT	* htable;
	
	if (Precondicion(hash) || Precondicion(data) )
	{
		Error("Lookup@hashADT.c - Invalid arguments\n");
		return -1;
	}
	
	if (HTableIsEmpty(hash))
	{
		Error("Lookup@hashADT.c - Hash table is empty\n");
		return -1;
	}
	
	pos = hash -> hkey(data, GetSlots(hash));
	
	htable = hash -> htable; 
	
	while ( iter != GetSlots(hash) && ( (htable[pos].status == OCCUPIED && hash -> hcmp(htable[pos].data, data) != 0 ) || htable[pos].status == LOGICAL ) )
		pos = (pos + 1) % GetSlots(hash);
	

	if (htable[pos].status == OCCUPIED && hash -> hcmp(htable[pos].data, data) == 0 )
		return pos;

	return -1;
}

int
GetSlots(hashADT hash)
{
	if ( Precondicion(hash) )
	{
		fprintf(stderr, "Invalid arguments to function GetSlots\n");
		return -1;
	}
	
	return hash -> tabledim;
}

int
GetNextElement(hashADT hash, hashElementT * element)
{
	int	aux = 1;
	
	if (Precondicion(hash)|| HTableIsEmpty(hash) || hash -> index < 0 || hash -> index >= hash -> tabledim)
	{
		Error("GetNextElement@hashADT.c - Invalid arguments to function GetNextElement\n");
		return 0;
	}
	
	
	while (hash -> index < hash -> tabledim && hash -> actual[hash -> index].status != OCCUPIED)
		hash -> index++;
	
	if (hash -> index >= hash -> tabledim)
		return 0;
		
	if (hash -> actual[hash -> index].status == OCCUPIED)
	{	
		if ( (*element = malloc(hash -> datasize)) == NULL )
		{
			Fatal("GetNextElement@hashADT.c - Not enough memory\n");
			return 0;
		}
		else
			memcpy(*element, hash -> actual[hash -> index++].data, hash -> datasize);
	}
			 
	return aux;
}

int 
SetHashBegin(hashADT hash)
{
	if (Precondicion(hash))
		return 0;
		
	hash -> index = 0;
	hash -> actual = hash -> htable;
	return 1;
}

int
HDelete(hashADT hash, hashElementT data)
{
	int pos;
	
	if ( (pos = Lookup(hash, data)) == -1 )
	{
		Error("HDelete@hashADT.c - Element not found, impossible to delete\n");
		return 0;
	}
	
	if ( hash -> htable[(pos + 1) % hash -> tabledim].status == PHYSICAL )
	{
		free(hash -> htable[pos].data);
		hash -> htable[pos].data = NULL;
		hash -> htable[pos].status = PHYSICAL;
	}
	else
		hash -> htable[pos].status = LOGICAL;
	
	return 1;
}

hashElementT 
GetHElement(hashADT hash, int pos)
{
	hashElementT aux = NULL;
	
	if ( Precondicion(hash) || Precondicion(hash -> htable) || pos < 0 || pos > hash -> tabledim)
	{
		Error("GetHElement@hashADT.c - invalid arguments\n");
		return NULL;
	}
	
	if (hash -> htable[pos].status == OCCUPIED)
	{
		if ( (aux = calloc(1, hash -> datasize)) == NULL )
		{
			Fatal("GetHElement@hashADT.c - not enough memory\n");
			Debug("GetHElement@hashADT.c - not enough memory during calloc for element\n");
		}
		
		memcpy(aux, hash -> htable[pos].data, hash -> datasize);
		return aux;
	}
	
	Debug("GetHElement@hashADT.c - no element in that position\n");
	
	return NULL;
}


int	
HTableIsEmpty(hashADT hash)
{
	if ( Precondicion(hash) )
	{
		fprintf(stderr, "Invalid arguments to function HTableIsEmpty\n");
		return -1;
	}


	
	return Precondicion(hash -> htable);
}

void
FreeHash(hashADT hash)
{
	int i;
	
	if (Precondicion(hash))
		return;
	
	for ( i = 0 ; i < hash -> tabledim ; i++ )
		if (hash -> htable[i].status != PHYSICAL )
			free(hash -> htable[i].data);
			
	free(hash -> htable);
	free(hash);
	return;
}

static int
Rehash(hashADT hash)
{
	int i, sizebkp;
	int iter = 0;
	htableT * newTable;
	htableT * backup;

	backup = hash -> htable;
	sizebkp = hash -> tabledim;
	
	/* se agranda con un numer impar del doble de dim */
	
	if ( (newTable = malloc((hash -> tabledim * 2 + 1) * sizeof(htableT))) == NULL )
		return 0;

	hash -> tabledim = sizebkp * 2 + 1;

	for (i = 0 ; i < hash -> tabledim ; i++)
		newTable[i].status = PHYSICAL;

	hash -> htable = newTable;
	hash -> qtty = 0; /* Reinicia la cantidad de elementos pues al re insertarlos incrementa en 1 */
	
	for (i = 0 ; i < sizebkp ; i++, iter = 0)
	{
		if (backup[i].status == OCCUPIED)
		{
			if (!HInsert(hash, backup[i].data))
			{
				hash -> htable = backup;
				hash -> tabledim = sizebkp;
				free(newTable);
				return 0;
			}
			free(backup[i].data);
		}
	}

	free(backup);
	return 1;
}

static int
InitTable(hashADT hash)
{
	if ( Precondicion(hash) )
		return 0;
		
	if ( (hash -> htable = calloc(TABLE_INIT_SIZE, sizeof(htableT))) == NULL )
	{
		fprintf(stderr, "Not enough memory to initiate hash table\n");
		return 0;
	}
	
	return TABLE_INIT_SIZE;
}
